﻿using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Net;

namespace SimpleLiteForm.References
{
    /// <summary>
    /// The search parameter used to perform a redirect.
    /// </summary>
    struct SearchParam
    {
        /// <summary>
        /// The cache key
        /// </summary>
        public string Key;

        /// <summary>
        /// The search result language.
        /// </summary>
        public CultureInfo Language;
    }

    /// <summary>
    /// A service to gather information from wikipedia.
    /// </summary>
    class WikiReferenceService : IReferenceService
    {
        /// <summary>
        /// The singleton instance.
        /// </summary>
        private static IReferenceService _instance;

        /// <summary>
        /// The web client.
        /// </summary>
        private WebClient _client;

        /// <summary>
        /// The last search results.
        /// </summary>
        private IList<ReferenceResult> _results = new List<ReferenceResult>();

        /// <summary>
        /// The reference service cache. The key looks like language->translatedword, e.g. en->optimization (eveything in lower case).
        /// </summary>
        private Dictionary<string, IList<ReferenceResult>> _cache = new Dictionary<string, IList<ReferenceResult>>();

        /// <summary>
        /// Indicates whether the service is loading content.
        /// </summary>
        private bool _isLoading; 

        /// <summary>
        /// The wikipedia extract format string, where {0} is the two-letter language code and {1} is the search term.
        /// </summary>
        private const string WIKI_URL_FORMAT = "https://{0}.wikipedia.org/w/api.php?action=query&prop=extracts&exsentences=4&titles={1}&format=json";

        /// <summary>
        /// Creates a new WikiReferenceService instance.
        /// </summary>
        private WikiReferenceService()
        {
            _client = new WebClient();
            _client.DownloadStringCompleted += (s, e) =>
                {
                    var param = (SearchParam)e.UserState;

                    if (e.Error != null)
                        return;

                    // parse result
                    var tmpResult = ParseJsonResult(e.Result, param.Language);

                    if (tmpResult.IsEmpty)
                    {
                        _isLoading = false;
                        Debug.WriteLine("No wiki result found.");
                    }
                    else
                    {
                        tmpResult.FilterContent();



                        // check for redirect
                        string redirectTerm = CheckForRedirect(tmpResult);

                        if (!string.IsNullOrEmpty(redirectTerm))
                        {
                            Debug.WriteLine(string.Format("[WIKI redirect] {0} --> {1}", tmpResult.Title, redirectTerm));

                            // do redirect
                            Search(redirectTerm, param.Language);
                            return;
                        }

                        // adjust "... may refer to:"
                        if (tmpResult.Content.TrimEnd().EndsWith(":"))
                            tmpResult.Content = string.Format("\"{0}\" is ambiguous. Please click the link to select.", tmpResult.Title);

                        _isLoading = false;

                        Debug.WriteLine(string.Format("[WIKI result] {0} : {1}", tmpResult.Title, tmpResult.Content));

                        // save search result
                        _results.Add(tmpResult);

                        // add to cache
                        _cache.Add(param.Key, _results);
                    }
                };
        }

        /// <summary>
        /// Searches for a term in a specific language.
        /// </summary>
        /// <param name="term">The search term.</param>
        /// <param name="language">The language.</param>
        public void Search(string term, CultureInfo language)
        {
            string key = string.Format("{0}->{1}", language.Name.ToLowerInvariant(), term.ToLowerInvariant());

            // check cache
            if (_cache.ContainsKey(key))
            {
                _isLoading = false;
                _results = _cache[key];
                return;
            }

            // create a new list
            // note: do not delete the last one, because it is stored in the cache
            _results = new List<ReferenceResult>();

            // correct serch term
            var searchTerm = term.Replace(" ", "%20");

            var uri = new Uri(string.Format(WIKI_URL_FORMAT, language.TwoLetterISOLanguageName, searchTerm), UriKind.Absolute);

            _isLoading = true;

            var param = new SearchParam();
            param.Key = key;
            param.Language = language;

            _client.DownloadStringAsync(uri, param);
        }

        /// <summary>
        /// Parses the JSON content.
        /// </summary>
        /// <param name="jsonContent">The JSON raw data of the search result.</param>
        /// <param name="language">The language of the JSON result.</param>
        /// <returns>The parsed and filtered search data or null if no result was found.</returns>
        private ReferenceResult ParseJsonResult(string jsonContent, CultureInfo language)
        {
            var obj = JObject.Parse(jsonContent);

            var pages = obj["query"]["pages"];

            if (pages == null)
                return ReferenceResult.Empty;

            var prop = (pages.First) as JProperty;
           
            var key = prop.Name;
            var value = prop.Value;

            if (key == "-1")
            {
                return ReferenceResult.Empty;
            }
            else
            {
                string title = value["title"].ToString();
                string content = value["extract"].ToString();
                string uriString = string.Format(
                    "http://{0}.wikipedia.org/wiki/{1}",
                    language.TwoLetterISOLanguageName,
                    title.Replace(' ','_'));

                return new ReferenceResult(title, content, new Uri(uriString, UriKind.Absolute));
            }
        }

        /// <summary>
        /// Checks and retrievs the redirect search term.
        /// </summary>
        /// <param name="tmpResult">The filtered search term.</param>
        /// <returns>The redirect search term or <code>string.Empty</code>.</returns>
        private string CheckForRedirect(ReferenceResult filteredResult)
        {
            const string redirectStart = "redirect ";

            string redirectTerm = filteredResult.Content.ToLower();

            if (redirectTerm.StartsWith(redirectStart))
                return redirectTerm.Remove(0, redirectStart.Length);
            else
                return string.Empty;
        }

        /// <summary>
        /// Clears the cache.
        /// </summary>
        public void ClearCache()
        {
            _cache.Clear();
        }

        /// <summary>
        /// Gets the referenc instance.
        /// </summary>
        public static IReferenceService Instance
        {
            get
            {
                if (_instance == null)
                    _instance = new WikiReferenceService();

                return _instance;
            }
        }

        /// <summary>
        /// Gets wheter there is a search result. This may take a while
        /// because the serach is done asynchronously.
        /// </summary>
        public bool HasResult
        {
            get
            {
                return _results.Count > 0;
            }
        }

        /// <summary>
        /// Gets wheter the async search is still going on.
        /// </summary>
        public bool IsLoading
        {
            get
            {
                return _isLoading;
            }
        }

        /// <summary>
        /// Gets the search results.
        /// </summary>
        public IList<ReferenceResult> Results
        {
            get
            {
                return _results;
            }
        }
    }
}
